<?php
/**
 * Copyright (C) 2013 Pablo Sepúlveda P. <psep.pentauc@gmail.com>
 *
 * This file is part of gAbstract.
 * WarpIT is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * any later version.
 *
 * gAbstract is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with gAbstract.  If not, see <http://www.gnu.org/licenses/>.
 */
/**
 * Clase que genera las Querys.
 *
 * @author psep
 * @version 1.0
 * @package gAbstract
 * @copyright Copyright (c) 2013 Pablo Sepúlveda P.
 * @license GNU GPLv3 or later
 * @link http://www.psep.cl
 */
class QueryBuilder {
	
	/**
	 * Objeto de tipo StringBuilder donde
	 * se almacenará la query construida.
	 *
	 * @var StringBuilder Object
	 */
	private $query;
	
	/**
	 * Atributo entero donde se cargará
	 * la opción para el tipo de query.
	 *
	 * @var Integer Object
	 */
	private $option;
	
	/**
	 * Atributo de tipo String donde se
	 * almacenará la variable inicial
	 * de la query en WHERE.
	 *
	 * @var String object
	 */
	private $_where;
	
	/**
	 * Objeto ArrayList que contiene una lista
	 * de objetos ParameterBean.
	 *
	 * @var ArrayList object
	 */
	private $parameters;
	
	/**
	 * Constructor de la Clase QueryBuilder
	 */
	public function __construct(){
		$this->reset();
	}
	
	/**
	 * Método para agregar el tipo del valor
	 * para ser cargado en el statement.
	 * Por defecto nada será cargado.
	 *
	 * @param mixed Object $value
	 * @param Integer $type
	 * 1 = Boolean |
	 * 2 = Null |
	 * 3 = Integer |
	 * 4 = String |
	 * 5 = SQL Date
	 */
	public function addParameter($value, $type = null){
		$parameter = new ParameterBean();
		$parameter->setValue($value);
		
		if($type == null){
			$type = 7;
		}
		
		$parameter->setType($type);
		$this->parameters->add($parameter);
	}
	
	/**
	 * Función que retorna lista con objetos ParameterBean
	 * que producen los statements.
	 *
	 * @return ArrayList Object
	 */
	public function getParameters(){
		return $this->parameters;
	}
	
	/**
	 * Método inicial y de reinicialización
	 * de la Clase QueryBuilder.
	 */
	public function reset(){
		$this->parameters	= new ArrayList();
		$this->query		= null;
		$this->_where		= null;
		$this->option		= 0;
	}
	
	/**
	 * Método que selecciona la opción de construcción
	 * de la query como un SELECT.
	 */
	public function createSelect(){
		$this->option = 1;
	}
	
	/**
	 * Método que selecciona la opción de construcción
	 * de la query como un UPDATE.  
	 */
	public function createUpdate(){
		$this->option = 2;
	}
	
	/**
	 * Método que selecciona la opción de construcción 
	 * de la query como un INSERT.
	 */
	public function createInsert(){
		$this->option = 3;
	}
	
	/**
	 * Método que selecciona la opción de construcción 
	 * de la query como un DELETE.
	 */
	public function createDelete(){
		$this->option = 4;
	}
	
	/**
	 * Método de construcción SELECT.
	 * Los parámetros de este método son para los campos
	 * que traerá la query y son cargados como un array().
	 * Si no se ingresa nada como parámetro la query será
	 * construida con un (*) trayendo todos los campos (ALLFIELDS).
	 * Ej: array('id_persona', 'nombre', 'direccion')
	 *
	 * @param array fields 
	 */
	public function select($select = null){
		if($this->option <> 1){
			throw new Exception("Error: No seleccionó la alternativa SELECT", 1);
		}else{
			$this->query = new StringBuilder();
			$this->query->append(SQLTemplate::$SELECT);
			$size = count($select);
			
			if($size < 1){
				$this->query->append(SQLTemplate::$ALLFIELDS);
			}else{
				$this->query->append(implode(", ", $select));
			}
			
		}
	}
	
	/**
	 * Método de construcción FROM. 
	 * Se ingresa el nombre de la tabla como un String. 
	 * Ej: 'persona' 
	 * 
	 * @param String object.
	 */
	public function from($from){
		if(!$this->validateWhere()){
			throw new Exception("Error: Opción from no corresponde", 1);
		}else if($from == null || trim($from) == ""){
			throw new Exception("Error: No ha ingresado tabla", 1);
		}else if($this->query == null){
			throw new Exception("Error: No existe instancia de Query", 1);
		}else{
			$this->query->append(SQLTemplate::$FROM);
			$this->query->append($from);
		}
		
	}
	
	/**
	 * Método que permite agregar INNER JOIN a la query.
	 * 
	 * @param String object (nombre de la tabla)
	 * @param String object (sintaxis alternativa)
	 */
	public function innerJoin($table, $other = null){
		$this->addJoin(SQLTemplate::$INNER, $table, $other);
	}
	
	/**
	 * Método que permite agregar LEFT JOIN a la query.
	 * 
	 * @param String object (nombre de la tabla)
	 * @param String object (sintaxis alternativa)
	 */
	public function leftJoin($table, $other = null){
		$this->addJoin(SQLTemplate::$LEFT, $table, $other);
	}
	
	/**
	 * Método privado que agrega sintaxis join.
	 * 
	 * @param String object (tipo de JOIN)
	 * @param String object (nombre de la tabla)
	 * @param String object (otra sintaxis de query anidada)
	 */
	private function addJoin($join, $table, $other = null){
		if($this->query == null){
			throw new Exception("Error: No existe instancia de query", 1);
		}else if($table == null || trim($table) == ""){
			throw new Exception("Error: El atributo de tabla es nulo", 1);
		}else if($this->option <> 1){
			throw new Exception("Error: La opción para SELECT no corresponde", 1);
		}else if($this->_where != null && trim($this->_where) != ""){
			throw new Exception("Error: Error de sintaxis en la query, revise WHERE", 1);
		}else{
			$this->query->append($join);
			$this->query->append(SQLTemplate::$JOIN);
			$this->query->append($table);
			
			if($other == null){
				$this->query->append($other);
			}
		}
	}
	
	/**
	 * Método de construcción WHERE. Se ingresa un parámetro como String 
	 * que será añadido a la query, por lo general de un campo. 
	 * Ej: 'id_persona = 1' 
	 * 
	 * @param String object.
	 */
	public function where($where){
		$this->_where = $where;
		
		if(!$this->validateWhere()){
			throw new Exception("Error: Opción where no corresponde", 1);
		}else if($this->_where == null || $this->_where == ""){
			throw new Exception("Error: No ha ingresado where", 1);
		}else if($this->query == null){
			throw new Exception("Error: No existe instancia de Query", 1);
		}else{
			$this->query->append(SQLTemplate::$WHERE);
			$this->query->append($this->_where);
		}
	}
	
	/**
	 * Método que valida para que opción corresponde utilizar el WHERE, 
	 * retornando un boolean true si ésto es correcto o un false 
	 * si está en un error. 
	 * 
	 * @return boolean
	 */
	private function validateWhere(){
		if($this->option == 0 || $this->option == 3){
			return false;
		}else{
			return true;
		}
	}
	
	/**
	 * Método que permite añadir campos correspondientes 
	 * al AND en la query mediante un array. 
	 * Ej: array('teléfono = ?', 'nombre = ?') 
	 * El resultado sería: 'teléfono = ? AND nombre = ?' 
	 * Siempre use Statements.
	 * 
	 * @param array
	 */
	public function andWhere($conditions = null){
		$this->concatWhere(SQLTemplate::$AND, $conditions);
	}
	
	/**
	 * Método que permite añadir campos correspondientes al 
	 * operativo OR en la query mediante un array como parámetro. 
	 * Ej: array('teléfono = 2331', 'nombre ="Pablo"') 
	 * El resultado sería: 'teléfono = 2331 OR nombre = 'Pablo'
	 * 
	 * @param array
	 */
	public function orWhere($conditions = null){
		$this->concatWhere(SQLTemplate::$OR, $conditions);
	}
	
	/**
	 * Método privado que permite concatenar arrays para 
	 * métodos que se relacionen con el operador WHERE. 
	 * 
	 * @param String object operator
	 * @param array fields.
	 */
	private function concatWhere($include, $conditions = null){
		if(!$this->validateWhere()){
			throw new Exception("Error: Opción where no corresponde", 1);
		}else if($conditions == null || count($conditions) == 0){
			throw new Exception("Error: No ha ingresado condiciones", 1);
		}else if($this->query == null){
			throw new Exception("Error: No existe instancia de Query", 1);
		}else{
			
			if($this->_where != null && trim($this->_where) != ""){
				$this->query->append($include);
			}else{
				$this->query->append(SQLTemplate::$WHERE);
			}
			$this->_where = SQLTemplate::$WHERE;
			$this->query->append(implode($include, $conditions));
		}
	}
	
	/**
	 * Método que agrega el operador ORDER BY al objeto $this->query. 
	 * Los campos son ingresados mediante parámetro de tipo array 
	 * y la opción: 0 = ASC (Ascedente) | 1 = DESC (Descendente).
	 * Por defecto agregará DESC.
	 * 
	 * @param array $orderBy fields 
	 * @param Integer $option
	 */
	public function orderBy($orderBy, $option){
		if($this->query == null){
			throw new Exception("Error: No existe instancia de Query", 1);
		}else if($orderBy == null || count($orderBy) == 0){
			throw new Exception("Error: No ha incluido order by", 1);
		}else if($option > 2){
			throw new Exception("Error: Debe señalar una alternativa válida {ASC = 0 | DESC = 1}", 1);
		}else if($this->option <> 1){
			throw new Exception("Error: No ha seleccionado SELECT QUERY", 1);
		}else{
			$this->query->append(SQLTemplate::$ORDERBY);
			$this->query->append(implode(", ", $orderBy));
			
			switch ($option) {
				case 0: $this->query->append(SQLTemplate::$ASC); break;
				default: $this->query->append(SQLTemplate::$DESC); break;
			}
		}
	}
	
	/**
	 * Método que agrega campos para el operador GROUP BY mediante 
	 * un array de campos que son enviados como parámetro.
	 * 
	 * @param array $groupBy fields
	 */
	public function groupBy($groupBy){
		if($this->query == null){
			throw new Exception("Error: No existe instancia de Query", 1);
		}else if($groupBy == null || count($groupBy) == 0){
			throw new Exception("Error: No ha incluido group by", 1);
		}else if($this->option <> 1){
			throw new Exception("Error: No ha seleccionado SELECT QUERY", 1);
		}else{
			$this->query->append(SQLTemplate::$GROUPBY);
			$this->query->append(implode(", ", $groupBy));
		}
	}
	
	/**
	 * Método que genera query INSERT en base a parámetros.
	 * Si desea insertar ID autocrementable debe dar como
	 * parámetro el autoincrement como true, por defecto
	 * inserta ID por variable.
	 * 
	 * @param array (lista de campos)
	 * @param String object (nombre de la tabla)
	 * @param boolean autoincrement
	 */
	public function insert($fields, $tableName, $autoincrement = false){
		if($this->option <> 3){
			throw new Exception("Error: La opción del INSERT no está seleccionada", 1);
		}else if(!is_array($fields)){
			throw new Exception("Error: El objeto \$fields no corresponde a una lista (array)", 1);
		}else if(count($fields) == 0){
			throw new Exception("Error: La lista \$fields está vacía", 1);
		}else{
			$this->query= new StringBuilder();
			$this->query->append(SQLTemplate::$INSERT);
			$this->query->append($tableName);
			$this->query->append(" (");
			
			if($autoincrement == true){
				unset($fields[0]);
			}
			
			$this->query->append(implode(", ", $fields));
			$this->query->append(") VALUES (");
			$value		= array();
			$size		= count($fields);
			
			for($i = 0; $i < $size; $i++){
				$value[] = "?";
			}
			
			$this->query->append(implode(", ", $value));
			$this->query->append(")");
		}
	}
	
	/**
	 * Método que genera query de INSERT a partir de un objeto.
	 * Éste retorna un array donde:
	 * array[0] = String query | array[1] = array fields value
	 * 
	 * @param Object
	 * @throws Exception
	 * @return array
	 */
	public function getInsert($object){
		if($this->option <> 3){
			throw new Exception("Error: No está seleccionada la opción Insert", 1);
		}else{
			$autoincrement 	= false;
			$id				= ObjectUtil::getIdObjectValue($object);
			$data			= ObjectUtil::attrValuesObject($object);
			$newData		= array();
			// Valida valor del ID, nulo aprueba autoincrement.
			if($id[1] == null || trim($id[1]) == ""){
				$autoincrement = true;
				unset($data[0]);
			}
			// Generación de Query
			$this->insert(ObjectUtil::getAttributesOfObject($object), get_class($object), $autoincrement);
			// Nuevo array de statements
			foreach ($data as $field => $value) {
				$newData[] = $value;
			}
			
			return array($this->getQuery(), $newData);
		}
		
		return null;
	}
	
	/**
	 * Método que genera query DELETE en el objeto StringBuilder
	 * encapsulado en QueryBuilder.
	 * 
	 * @param String object $tableName
	 * @param Array conditionsAnd (fields)
	 * @param Array conditionsOr (fields)
	 */
	public function delete($tableName, $conditionsAND = null, $conditionsOR = null){
		if($this->option <> 4){
			throw new Exception("Error: No está seleccionada la opción DELETE", 1);
		}else if($tableName == null ||trim($tableName) == "" ){
			throw new Exception("Error: Debe ingresar nombre de la tabla", 1);
		}else{
			$this->query = new StringBuilder();
			$this->query->append(SQLTemplate::$DELETE);
			$this->from($tableName);
			
			if($conditionsAND != null && is_array($conditionsAND) && count($conditionsAND) > 0){
				$this->andWhere($conditionsAND);
			}
			
			if($conditionsOR != null && is_array($conditionsOR) && count($conditionsOR) > 0){
				$this->orWhere($conditionsOR);
			}
		}
	}
	
	/**
	 * Método que construye a través de un objeto la query
	 * correspondiente para DELETE y es retornada en un array
	 * con la siguiente estructura:
	 * array[0] = String query | array[1] = array fields value
	 * 
	 * @param Object
	 * @throws Exception
	 * @return array
	 */
	public function getDeleteDefault($object){
		if($this->option <> 4){
			throw new Exception("Error: No está seleccionada la opción Delete", 1);
		}else{
			$data			= ObjectUtil::searchValueObject($object);
			$conditionsAnd	= array();
			$statements		= array();
			
			foreach ($data as $attr => $value) {
				$conditionsAnd[]= $value[0];
				$statements[]	= $value[1];
			}
			
			// Generación de Query
			$this->delete(get_class($object), $conditionsAnd);
			
			return array($this->getQuery(), $statements);
		}
		
		return null;
	}
	
	/**
	 * Método que genera query UPDATE en el objeto StringBuilder
	 * encapsulado en QueryBuilder.
	 * 
	 * @param String object $tableName
	 * @param Array SET
	 * @param Array conditionsAnd (fields)
	 * @param Array conditionsOr (fields)
	 */
	public function update($tableName, $set, $conditionsAND = null, $conditionsOR = null){
		if($this->option <> 2){
			throw new Exception("Error: No está seleccionada la opción UPDATE", 1);
		}else if($tableName == null ||trim($tableName) == "" ){
			throw new Exception("Error: Debe ingresar nombre de la tabla", 1);
		}else if($set == null || count($set) == 0){
			throw new Exception("Error: No hay campos en argumento SET", 1);
		}else{
			$this->query = new StringBuilder();
			$this->query->append(SQLTemplate::$UPDATE);
			$this->query->append($tableName);
			$this->query->append(SQLTemplate::$SET);
			$this->query->append(implode(", ", $set));
			
			if($conditionsAND != null && is_array($conditionsAND) && count($conditionsAND) > 0){
				$this->andWhere($conditionsAND);
			}
			
			if($conditionsOR != null && is_array($conditionsOR) && count($conditionsOR) > 0){
				$this->orWhere($conditionsOR);
			}
		}
	}
	
	/**
	 * Método que construye a través de un objeto la query
	 * correspondiente para UPDATE y es retornada en un array
	 * con la siguiente estructura:
	 * array[0] = String query | array[1] = array fields value
	 * 
	 * La actualización se produce mediante ID por defecto,
	 * si el ID de la entidad viene nulo la actualización será
	 * para toda la tabla.
	 * 
	 * @param Object
	 * @throws Exception
	 * @return array
	 */
	public function getUpdateDefault($object){
		if($this->option <> 2){
			throw new Exception("Error: No está seleccionada la opción Update", 1);
		}else{
			// $data			= ObjectUtil::searchValueObject($object);
			$set			= array();
			$conditionsAnd	= array();
			$statements		= array();
			
			$fields = ObjectUtil::getAttributesOfObject($object);
			$values = ObjectUtil::attrValuesObject($object);
			
			if(count($fields) <> count($values)){
				throw new Exception("Error en la serialización del objeto", 1);
			}
						
			for($i = 0; $i < count($fields); $i++){
				if($i == 0){
					$conditionsAnd[] = $fields[$i]." = ?";
				}else{
					$set[]			= $fields[$i]." = ?";
					$statements[]	= $values[$i];
				}
				
			}
			
			if ($values[0] != NULL && trim($values[0]) != "") {
				$statements[] = $values[0];
			}else{
				$conditionsAnd = array();
			}
			
			// Generación de Query
			$this->update(get_class($object), $set, $conditionsAnd);
			
			return array($this->getQuery(), $statements);
		}
		
		return null;
	}
	
	/**
	 * Método que retorna el objeto $this->query de 
	 * tipo StringBuilder como un String mediante al toString(). 
	 * 
	 * @return String object
	 */
	public function getQuery(){
		if($this->query == null){
			return null;
		}else{
			return $this->query->toString();
		}
	}
	
	/**
	 * Método que permite generar una nueva query personalizada.
	 * 
	 * @param String object.
	 */
	public function setQuery($sql){
		if($this->option == 0 || $this->option > 4){
			throw new Exception("Error: Debe seleccionar una opción de query válida", 1);
		}else{
			$this->query = new StringBuilder();
			$this->query->append($sql);
		}
	}
	
	/**
	 * Método Getter para $option
	 * 
	 * @return Integer option
	 */
	public function getOption(){
		return $this->option;
	}
	
}

?>
